Skip to content

feat: rich chat notifications with inline transcript, reply, and Send Cash#416

Open
raulriera wants to merge 19 commits into
mainfrom
feat/rich-chat-notifications
Open

feat: rich chat notifications with inline transcript, reply, and Send Cash#416
raulriera wants to merge 19 commits into
mainfrom
feat/rich-chat-notifications

Conversation

@raulriera

Copy link
Copy Markdown
Collaborator

Summary

Adds a rich notification for incoming chat messages. Expanding a chat push now shows the recent conversation inline — message bubbles and cash cards that display the token's name and icon — with inline reply and a Send Cash action that opens the amount sheet for the sender. The notification extension runs without the full app session: the owner key is shared into a keychain access group, and chat messages and token metadata are fetched over the network, so the extension needs no database.

Stacked on #395 (App Intents) — that commit appears in this diff until #395 merges, after which this branch rebases down to just the notification changes.

Test plan

  • Receiving a chat message shows the rich notification with the recent transcript.
  • A cash message shows the card with the correct token name and icon.
  • Replying from the notification sends the message and updates the transcript.
  • Tapping Send Cash opens the amount sheet for the right recipient.
  • After a fresh install, notifications render once the device has been unlocked.

raulriera added 19 commits June 25, 2026 09:52
…estions

Index DM chats into CoreSpotlight (with avatars) so they're searchable and
open on tap, donate open chats as Siri Suggestions/Handoff, and add a Send
Cash app shortcut that opens the send flow for a contact. Spotlight taps and
the shortcut route through the app's existing deep-link navigation.
- PushController registers the CHAT_MESSAGE category on init with
  Reply (text-input) and Send Cash (.foreground) actions
- didReceive posts flipcash://chat/{id}/send to the deep-link pipeline
  when the Send Cash action is tapped, decoding the chat ID from the
  push payload navigation
- Route.Path gains .chatSendCash(ConversationID); /chat/{id}/send
  parses to it while /chat/{id} stays .chat
- DeepLinkController handles .chatSendCash: resolves the conversation
  counterpart via ChatNotificationRouter and navigates to .sendAmount
  when canSend is true
- ScanViewModel.canScanQR updated for exhaustive switch
- RouteTests gains two cases verifying /chat/{id}/send and /chat/{id}
  parse correctly
… migration

Route the owner account (.currentUserAccount) through a runtime-derived
shared keychain access group so a notification extension can authenticate.
Reads fall back to the legacy groupless location and a one-time migration
copies the existing item into the shared group, guaranteeing no logout for
existing users on upgrade.
…cation previews

Public FlipcashUI extension that maps [ConversationMessage] to the last 3
ChatItems (sorted chronologically, oldest-first) — usable by the notification
content extension which cannot link the main app target.
Bridges the shared keychain access group to the notification content
extension, which has no access to the main app's private group.
Notification content extension (CHAT_MESSAGE category) linking FlipcashCore +
FlipcashUI, embedded in the app, with the shared keychain-access-group
entitlement. Stub view controller; content rendering follows.
- OwnerKeyStore.loadOwnerAccount() returns both KeyPair and UserID; loadOwnerKeyPair() delegates to it
- NotificationPayload.chatConversationID(from:) extracts ConversationID without requiring FlipcashAPI import in the extension
- NotificationViewController fetches last 3 messages on push receipt, renders ChatMessageCell bubbles via UICollectionView + compositional list layout, handles inline reply (sendMessage + append bubble) and Send Cash action forwarding
- Bumped NotificationContent deployment target to 18.0 (matches FlipcashCore requirement)
Track the status label and remove the prior one before showing a new one;
clear it when content loads. A fetch error followed by a send error no longer
stacks two overlapping labels.
…leak, analytics gate, decode path

C1: Keychain.set with an accessGroup now inserts .accessible(.afterFirstUnlock) so the shared
owner-account item is readable by the notification extension while the device is locked.
The resolveTeamPrefix probe item gets the same accessibility so team-prefix resolution
also works in that context.

I1: NotificationViewController now holds a single lazy ChatNotificationClient instead of
creating one in each didReceive call, preventing NIO event-loop-group leaks.
ChatNotificationClient gains @unchecked Sendable to satisfy Swift 6 actor-isolation checks.

Minor: Analytics.deeplinkRouted for .chatSendCash now fires only when the route proceeds
(inside the canSend guard). PushController.didReceive now uses
NotificationPayload.chatConversationID instead of inline decode.
- Link UserNotificationsUI.framework so the content extension actually loads
  (it was being killed at launch: missing extension-point framework)
- Force dark mode + paint the chat background so the white bubbles are visible
- Render cash messages via ChatCashCardCell with currency + flag, not blank bubbles
- Match the real chat's bubble spacing and width
- Poll while expanded so the counterparty's new messages appear live
Open the chat as the root sheet then stack Send Cash on top (present(.conversation)
+ presentNested(.sendAmount)), matching #402, so it always lands back in the chat.
Repoint callers to main's NotificationPayload.chatID(_:) (drop the duplicate helper)
and update the preview switch for main's ChatItem (no .receipt).
Add present(_:animated:) to AppRouter; the notification Send Cash deeplink and
the App Intents send now mount the chat instantly (animated: false) and animate
only the Send Cash amount sheet on top, so it opens directly with the chat
already behind it instead of sliding the chat in first.
The notification Send Cash deeplink and the App Intents send now present
.sendAmount as a root sheet — one animation, no chat slide-up behind it.
Render .sendAmount at the ScanScreen root (was EmptyView, nested-only) and
expose SendAmountSheetRoot. Reverts the no-op present(animated:) (disablesAnimations
does not suppress a SwiftUI sheet's presentation animation).
Embed ChatViewController so the expanded notification sizes to its content,
scrolls, and renders every cell (including the cash card) like the in-app
chat. Hide the default notification content so the message isn't shown
twice, and match the extension version to the app so iOS loads the extension.
Resolve mint metadata over the network from the extension (lean CurrencyService
on the payments host, cached per mint) and render it without the diff sliding it
in. Plus rich-notification cleanup: dedupe the send-target construction, the
keychain test helper, and the transcript reload path; tidy comments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant